# -*- coding: utf-8 -*-
# @Time    : 2024/6/13 10:56
# @Author  : yujiahao
# @File    : 20_decorator.py
# @description:Python中的装饰器


# todo 1、开放封闭原则

'''
在讲解装饰器之前的时候我们先讲解一下开放封闭原则

什么是开放封闭原则？有的人就问开放，封闭这是两个反义词这还能组成一个原则么？这不前后矛盾么？其实不矛盾。开放封闭原则是分情况讨论的。

我们的软件一旦上线之后（比如你的软件主要是多个函数组成的）,那么这个软件对功能的扩展应该是开放的，比如你的游戏一直在迭代更新，推出新的玩法，新功能。但是对于源代码的修改是封闭的。
你就拿函数举例，如果你的游戏源代码中有一个函数是闪躲的功能，那么你这个函数肯定是被多个地方调用的，
比如对方扔手雷，对方开枪，对方用刀，你都会调用你的闪躲功能，那么如果你的闪躲功能源码进行改变了，或者调用方式改变了，当对方发起相应的动作，你在调用你的闪躲功能，就会发生问题。
所以，开放封闭原则具体定义是这样：

 1.对扩展是开放的

    我们说，任何一个程序，不可能在设计之初就已经想好了所有的功能并且未来不做任何更新和修改。所以我们必须允许代码扩展、添加新功能。

 2.对修改是封闭的

    就像我们刚刚提到的，因为我们写的一个函数，很有可能已经交付给其他人使用了，如果这个时候我们对函数内部进行修改，或者修改了函数的调用方式，很有可能影响其他已经在使用该函数的用户。OK，理解了开封封闭原则之后，我们聊聊装饰器。


由此在Python中就是有个东西叫装饰器，从字面意思来分析，先说装饰，什么是装饰? 装饰就是添加新的，

比如我现在不会飞，怎么才能让我会飞？给我额外增加一个翅膀，我就能飞了。那么你给我加一个翅膀，它会改变我原来的行为么？我之前的吃喝拉撒睡等生活方式都不会改变。
说白了它就是在我原来的基础上，添加了一个新的功能。今天我们讲的装饰器（翅膀）是以功能为导向的，就是一个函数。
被装饰的对象：我本人，其实也是一个函数。

所以装饰器最终最完美的定义就是：在不改变原被装饰的函数的源代码以及调用方式下，为其添加额外的功能。

'''

"""
Python, Java, 和 Scala 中的装饰器或类似机制的对比、定义、使用方法、注意事项及分类：

1. Python 的装饰器:
   定义：在 Python 中，装饰器是用来修改其他函数的功能的函数。它们本质上是一个接收函数作为参数并返回一个新函数的函数。

   使用方法：
   def my_decorator(func):
       def wrapper(*args, **kwargs):
           print("Something is happening before the function is called.")
           result = func(*args, **kwargs)
           print("Something is happening after the function is called.")
           return result
       return wrapper

   @my_decorator
   def say_hello():
       print("Hello!")

   say_hello()

   注意事项：
   - 确保装饰器内部使用 `functools.wraps`，以保留原函数的名字和文档字符串。
   - 注意装饰器的执行顺序，特别是当应用多个装饰器时。

   分类：
   - 函数装饰器：直接作用于函数。
   - 类装饰器：作用于类，可以修改类的行为。

2. Java 的装饰器模式:
   定义：装饰器模式（Decorator Pattern）允许向一个现有的对象添加新的功能，同时又不改变其结构。这种类型的设计模式属于结构型模式。

   使用方法：
   public interface Coffee {
       double getCost();
       String getIngredients();
   }

   public class SimpleCoffee implements Coffee {
       public double getCost() {
           return 1;
       }
       public String getIngredients() {
           return "Coffee";
       }
   }

   public class MilkCoffeeDecorator implements Coffee {
       private Coffee decoratedCoffee;
       public MilkCoffeeDecorator(Coffee coffee) {
           this.decoratedCoffee = coffee;
       }
       public double getCost() {
           return decoratedCoffee.getCost() + 0.5;
       }
       public String getIngredients() {
           return decoratedCoffee.getIngredients() + ", Milk";
       }
   }

   注意事项：
   - 装饰器应该只添加功能，不应改变原有对象的接口。
   - 注意装饰链的管理，避免过多层的装饰导致系统复杂。

   分类：
   - 接口装饰：通过实现同一接口增加功能。
   - 抽象装饰：通过抽象类实现基本装饰逻辑，具体功能由子类实现。

3. Scala 的特质和隐式转换:
   定义：Scala 中的特质可以用来装饰类，隐式转换可以用来自动应用这些特质。

   使用方法：
   trait Coffee {
       def getCost(): Double
       def getIngredients(): String
   }

   class SimpleCoffee extends Coffee {
       def getCost() = 1.0
       def getIngredients() = "Coffee"
   }

   trait MilkDecorator extends Coffee {
       abstract override def getCost() = super.getCost() + 0.5
       abstract override def getIngredients() = super.getIngredients() + ", Milk"
   }

   val myCoffee = new SimpleCoffee with MilkDecorator

   注意事项：
   - 使用 `abstract override` 需要小心，确保特质的顺序正确。
   - 隐式转换应谨慎使用，以避免意外的行为和性能问题。

   分类：
   - 特质装饰：通过特质扩展类的功能。
   - 隐式装饰：通过隐式转换自动应用特质。

总结:
Python 提供了灵活直接的装饰器语法，适合快速开发和动态行为修改。
Java 的装饰器模式更加正式，适合需要稳定和可维护代码的大型软件项目。
Scala 的特质和隐式转换提供了强大的装饰能力，适合需要高度可扩展和灵活的系统设计。
"""



# todo 2、 初识装饰器

# 接下来，我们通过一个例子来为大家讲解这个装饰器

'''需求介绍：你现在xx科技有限公司的开发部分任职，领导给你一个业务需求让你完成：让你写代码测试小明同学写的函数的执行效率。'''


#小明的代码
def index():
    time.sleep(2)  # 模拟一下网络延迟以及代码的效率
    print('欢迎访问博客园主页')
# 小华的代码
def home(name):
    time.sleep(3)  # 模拟一下网络延迟以及代码的效率
    print(f'欢迎访问{name}主页')

''' 版本1：如果要测试这个执行效率，就需要记录代码运行前后的时间，然后减掉，可以导入time模块【后面再说，Java Scala叫导包，下同】获取时间，如下：'''

import time

start = time.time()
index()
end = time.time()
print(f'该程序运行时间为：{end - start}')

'''版本2：如果测一个的话可以这么写，要是测十几个代码呢，这样就显得冗余了，为了减少重复代码，我们可以封装成函数如下：'''

def inner():
    start_time = time.time()
    index()
    end_time = time.time()
    print(f'此函数的执行效率为{end_time-start_time}')

inner()

'''版本3：如果要是像上面那么做，每次测试其他同事的代码还需要手动改，这样也不行，这里可以使用将被装饰函数的函数名作为函数的参数传递进去，动态传参：'''

def timmer(func):  # func == index 函数
    start_time = time.time()
    func()  # index()
    end_time = time.time()
    print(f'此函数的执行效率为{end_time-start_time}')

timmer(index)


'''这样将index函数的函数名作为参数传递给timmer函数，然后在timmer函数里面执行index函数，这样就变成动态传参了。
        
        对比着开放封闭原则说： 
            首先，index函数除了完成了自己之前的功能，还增加了一个测试执行效率的功能，所以也符合开放原则。 
            其次，index函数源码也没有变，但是执行方式改变了，所以不符合封闭原则。因为原来直接调用ndex()就可以执行，但是现在的话调用方式变了需要调用inner(index)执行,
            这样会造成一个问题，假如index在你的项目中被100处调用，那么这相应的100处调用我都得改成inner(index)。 非常麻烦，也不符合开放封闭原则。
'''

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')


'''版本4：实现真正的开放封闭原则：装饰器。也就是前面说的【闭包】，将上面的inner函数在套一层最外面的函数timer，将inner函数名作为最外面的函数的返回值：'''


def timer(func):  # func = index
    def inner():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner
f = timer(index)
f()

'''分析一下代码:
    1、代码执行到这一行：f = timer(index) 看见一个等号先要执行等号右边， 
    2、timer(index) 执行timer函数将index函数名传给了func形参。
    3、内层函数inner不执行，inner函数返回 给f变量。
    4、所以我们执行f() 就相当于执行inner闭包函数。 
    5、f(),这样既测试效率又执行了原函数，还是有点问题，版本4 要解决原函数执行方式不改变的问题， 
    6、所以你可以把 f 换成 index变量就完美了！ index = timer(index)注意下，函数外面的index实际是inner函数的内存地址而不是index函数。

通过上面的例子可以看到，这个timer就是最简单版本装饰器，在不改变原index函数的源码以及调用方式前提下，为其增加了额外的功能，测试执行效率。
'''


print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')


# todo 二、标准版装饰器

def home(name,age):
    time.sleep(3)  # 模拟一下网络延迟以及代码的效率
    print(name,age)
    print(f'欢迎访问{name}主页')

def timer(func):  # func = home
    def inner(*args,**kwargs):
        start_time = time.time()
        func(*args,**kwargs)
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner

home = timer(home)
home('李白',18)


'''最终版本：标准版装饰器，使用语法糖

 - 按照上面的例子，虽然说是已经给函数加上了装饰器，但是每次调用的时候，还要写上一句：home = timer(home)
 - 这样你在执行home函数 home('太白',18) 才是真生的添加了额外的功能。如果调用很多，每次写这一句也是很麻烦。
 - 所以，Python给我们提供了一个简化机制，用一个很简单的符号去代替这一句话。在原函数上增加一个注释: @装饰器的名字
    > 注意装饰器要放在原函数的上面，否则找不到方法
 
 - 至此标准版的装饰器就是这个样子：完全符合代码开放封闭原则
       
        def wrapper(func):
            def inner(*args,**kwargs):
                '执行被装饰函数之前的操作'
                ret = func
                '执行被装饰函数之后的操作
                return ret
            return inner
'''

def timer(func):  # func = home
    def inner(*args,**kwargs):
        start_time = time.time()
        func(*args,**kwargs)
        end_time = time.time()
        print(f'此函数的执行效率为{end_time-start_time}')
    return inner

@timer  # home = timer(home)
def home(name,age):
    time.sleep(3)  # 模拟一下网络延迟以及代码的效率
    print(name,age)
    print(f'欢迎访问{name}主页')

home('杜甫',18)



print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')


# todo 做个需求：简单版模拟用户登录

'''
需求： 用户登陆之后有几个页面，diary，comment，home，如果要访问这几个页面，必须验证是否已登录。 
如果已经成功登录，那么这几个页面我都可以无阻力访问。
如果没有登录，任何一个页面都不可以访问，必须先登录，登录成功之后，才可以访问这个页面。
用成功执行函数模拟作为成功访问这个页面，现在写三个函数，写一个装饰器，实现上述功能。


解题步骤：
1、分析发现，需要三个函数，这三个函数都有同样的功能，就是验证登录，由此将验证登录作为装饰器，在三个函数中调用
2、预设登录用户名和登录状态
3、书写装饰器，进行判断
    1> 如果用户本身已经登录了，那么直接执行形参函数，并返回该函数形参
    2> 如果没有登录，就要输入秘钥，然后才可以置为登录状态
4、使用语法糖在函数中调用装饰器

'''

# 用来存用户名和登录状态
login_status = {
    'username': None,
    'status': False,
}

def auth(fun):
    def inner(*args, **kwargs):
        while True:  # 使用循环来允许多次尝试登录
            # 如果已经登录，直接执行
            if login_status['status']:
                return fun(*args, **kwargs)
            # 我这里单独增加了一个退出的
            username = input('请输入用户名或输入exit退出：').strip()
            if username.lower() == 'exit':
                print('退出登录！')
                return

            password = input('请输入密码或输入exit退出：').strip()
            if password.lower() == 'exit':
                print('退出登录！')
                return

            if username == 'yujiahao' and password == '123456':
                login_status['status'] = True
                return fun(*args, **kwargs)
            else:
                print('您输入的信息有误，请核实后重新输入！如需结束，请输入exit')

    return inner



@auth
def diary():
    print('欢迎访问日记页面')

@auth
def comment():
    print('欢迎访问评论页面')

@auth
def home():
    print('欢迎访问博客园主页')

# 分别调用，只输入一次
diary()
comment()
home()


print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')


# todo 三、带参数的装饰器

'''。
有下面的场景：
     - 我们使用APP过程中有可能需要第三方授权的账号进行登录：抖音：绑定的是微信账号密码。 皮皮虾：绑定的是qq的账号密码。 
     - 但是之前写的装饰器只能接受一个参数就是函数名:
            def auth(func):
                def inner(*args,**kwargs):
                    if login_status['status']:
                        ret = func()
                        return ret
                    username = input('请输入用户名：').strip()
                    password = input('请输入密码：').strip()
                    if username == '太白' and password == '123':
                        login_status['status'] = True
                        ret = func()
                        return ret
            return inner
    - 装饰器其实就是一个闭包函数，再说简单点就是两层的函数。那么是函数，就应该具有函数传参功能，
      所以，我们现在需要一个需要一个可以接受参数的装饰器:
        
        def auth(x):
            def auth2(func):
                def inner(*args,**kwargs):
                    if login_status['status']:
                        ret = func()
                        return ret
                    username = input('请输入用户名：').strip()
                    password = input('请输入密码：').strip()
                    if username == '太白' and password == '123':
                        login_status['status'] = True
                        ret = func()
                        return ret
                return inner
            return auth2

    - 并且装饰器要分情况去判断账号和密码，不同的函数用的账号和密码来源不同:
       
        def auth2(func):
            def inner(*args, **kwargs):
                if login_status['status']:
                    ret = func()
                    return ret
                if 微信:
                    username = input('请输入用户名：').strip()
                    password = input('请输入密码：').strip()
                    if username == '太白' and password == '123':
                        login_status['status'] = True
                        ret = func()
                        return ret
                elif 'qq':
                    username = input('请输入用户名：').strip()
                    password = input('请输入密码：').strip()
                    if username == '太白' and password == '123':
                        login_status['status'] = True
                        ret = func()
                        return ret
            return inner
    
    @auth2
    def jitter():
        print('记录美好生活')
    
    
    @auth2
    def pipefish():
        print('期待你的内涵神评论')


'''

# 那么最终的如下：
'''
@auth('wechat') :分两步：

 第一步先执行auth('wechat')函数，得到返回值auth2

 第二步@与auth2结合，形成装饰器@auth2 然后在依次执行。

这样就是带参数的装饰器，参数可以传入多个，一般带参数的装饰器在以后的工作中都是给你提供的， 你会用就行，但是自己也一定要会写，面试经常会遇到。

'''

def auth(x):
    def auth2(func):
        def inner(*args, **kwargs):
            if login_status['status']:
                ret = func()
                return ret

            if x == 'wechat':
                username = input('请输入用户名：').strip()
                password = input('请输入密码：').strip()
                if username == '豪哥' and password == '111111':
                    login_status['status'] = True
                    ret = func()
                    return ret
            elif x == 'qq':
                username = input('请输入用户名：').strip()
                password = input('请输入密码：').strip()
                if username == '宇哥' and password == '111111':
                    login_status['status'] = True
                    ret = func()
                    return ret
        return inner
    return auth2

@auth('wechat')
def jitter():
    print('记录美好生活')

@auth('qq')
def pipefish():
    print('期待你的内涵神评论')

pipefish()





# todo 四、装饰器装饰多个函数

'''Python规定多个装饰器装饰一个函数的时候先执行离被装饰的函数最近的装饰器，说白了就是写在你被装饰函数上面的注释，是按顺序从上到下执行的,
这就是函数的调用和值传递的过程（外部函数首先执行并返回内部函数，然后内部函数被调用），没啥滑头，你可以将装饰器想象为一个包装纸，当你有多层包装时，
最内层的包装（最接近礼物本身的那层）首先被放置，然后是外层的包装。当你开始打开包装时，你首先移除最外层的包装纸，然后依次向内层移动，直至到达最内层。

【说明】
假设你有两个装饰器，wrapper1 和 wrapper2，以及一个函数 func：

    @wrapper1
    @wrapper2
    def func():
        print("这是被装饰的函数")
        
在这个例子中，装饰器的应用顺序如下：

     - wrapper2 首先应用到 func 函数上。
     - 然后，wrapper1 应用到了 wrapper2 的结果上。
     - 这意味着，实际上的执行顺序是 wrapper1(wrapper2(func))()。
        这里，wrapper2 首先对 func 进行包装，然后 wrapper1 再对 wrapper2 的结果进行包装。

'''


def wrapper1(func):  # 2 这里的形参就是：wrapper2(func)
    def inner1(*args, **kwargs):
        print("这是装饰器一开始")  # 3 先打印这个
        func(*args, **kwargs)  # 4 这里执行func 也就是：wrapper2(func)，执行完之后返回到这里
        print("这是装饰器一结束")  # 13打印这个

    return inner1  # 14 返回这个，全部结束


def wrapper2(func):  # 6 到这里就是执行的是 func
    def inner2(*args, **kwargs):
        print("这是装饰器二开始")  # 7 打印这个
        func(*args, **kwargs)  # 8 这里执行func 也就是被装饰的函数func()，执行完成之后返回到这里
        print("这是装饰器二结束")  # 11 打印这个

    return inner2  # 12 返回到wrapper1中，wrapper2结束


# 按照被装饰的顺序有上到下依次执行
# 先执行@wrapper1
# 在执行@wrapper2
# 翻译一下就是：wrapper1(wrapper2(func))()
@wrapper1  # 1 先执行这个，将wrapper2(func)作为参数传递进去
@wrapper2  # 5 这个作为 @wrapper1 中inner1调用的参数
def func():  # 9 执行这个
    print("这是被装饰的函数")  # 10 打印这个


func()  # 0 调用被装饰函数，实际上是调用 wrapper1(wrapper2(func))()。

